package org.sqlproc.engine.impl;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A META SQL sub-element. It represents a dynamic input value.
 * 
 * <p>
 * Schematically:
 * 
 * <pre>
 * SqlMetaIdent
 *     +-SqlMetaIdentItem.SqlMetaIdentItem...^SqlMyType^value
 * </pre>
 * 
 * @author <a href="mailto:Vladimir.Hudec@gmail.com">Vladimir Hudec</a>
 */
class SqlMetaIdent implements SqlMetaSimple, SqlMetaLogOperand {

    /**
     * The internal slf4j logger.
     */
    final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Which conversion should be done on input value.
     */
    private SqlInputValue.Case caseConversion;
    /**
     * An indicator, which is used to control, how the input value is added to the final ANSI SQL. A standard behavior
     * is to add an input value only in the case it's not empty. The definition of the emptiness depends on the type of
     * the input value.
     */
    private boolean not;
    /**
     * The list of sub-elements. Every sub-element represents the name of an attribute in the input class (the static
     * parameters class). In case there're more names, the input classes are embedded one in other.
     */
    private List<SqlMetaIdentItem> elements;
    /**
     * The type of this input value. It can be Hibernate or an internal type.
     */
    private SqlType sqlType;

    /**
     * Creates a new instance of this entity. Used from inside ANTLR parser.
     * 
     * @param caseConversion
     *            which conversion should be done on inputValue
     */
    SqlMetaIdent(SqlInputValue.Case caseConversion) {
        this(caseConversion, false);
    }

    /**
     * Creates a new instance of this entity. Used from inside ANTLR parser.
     * 
     * @param caseConversion
     *            which conversion should be done on inputValue
     * @param not
     *            an indicator, which is used to control, how the input value is added to the final ANSI SQL
     */
    SqlMetaIdent(SqlInputValue.Case caseConversion, boolean not) {
        this(caseConversion, false, new SqlType());
    }

    /**
     * Creates a new instance of this entity. Used from inside ANTLR parser.
     * 
     * @param caseConversion
     *            which conversion should be done on inputValue
     * @param not
     *            an indicator, which is used to control, how the input value is added to the final ANSI SQL
     * @param type
     *            the type of this input value, which can be Hibernate or an internal type
     */
    SqlMetaIdent(SqlInputValue.Case caseConversion, boolean not, SqlType type) {
        this.elements = new ArrayList<SqlMetaIdentItem>();
        this.caseConversion = caseConversion;
        this.not = not;
        this.sqlType = type;
    }

    /**
     * Adds a new name. This is the name of an attribute in the input class (the static parameters class). In case
     * there're more names, the input classes are embedded one in other.
     * 
     * @param name
     *            the next name in the list of names
     */
    void addIdent(String name) {
        int size = elements.size();
        SqlMetaIdentItem lastItem = (SqlMetaIdentItem) (size > 0 ? elements.get(size - 1) : null);
        if (lastItem != null)
            lastItem.setType(SqlMetaIdentItem.Type.REF);
        elements.add(new SqlMetaIdentItem(name, SqlMetaIdentItem.Type.VAL));
    }

    /**
     * Adds a new name. This is the name of an attribute in the input class (the static parameters class). In case
     * there're more names, the input classes are embedded one in other.
     * 
     * @param element
     *            an object representation of the next name in the list of names
     */
    void addIdent(SqlMetaIdentItem element) {
        elements.add(element);
    }

    /**
     * Sets the internal type of this input value.
     * 
     * @param sMetaType
     *            a String representation of the internal type
     */
    void setMetaType(String sMetaType) {
        this.sqlType = new SqlType(sMetaType, this.sqlType);
    }

    /**
     * Sets Hibernate type of this input value.
     * 
     * @param sHibernateType
     *            a String representation of Hibernate type
     */
    void setHibernateType(String sHibernateType) {
        this.sqlType = new SqlType(sHibernateType);
    }

    /**
     * Returns the type of this input value. It can be Hibernate or an internal type.
     * 
     * @return the type of this input value
     */
    SqlType getSqlType() {
        return this.sqlType;
    }

    /**
     * Sets the indicator, which is used to control, how the input value is added to the final ANSI SQL. A standard
     * behavior is to add an input value only in the case it's not empty. The definition of the emptiness depends on the
     * type of the input value.
     * 
     * @param not
     *            a new indicator value
     */
    void setNot(boolean not) {
        this.not = not;
    }

    /**
     * Returns the indicator, which is used to control, how the input value is added to the final ANSI SQL. A standard
     * behavior is to add an input value only in the case it's not empty. The definition of the emptiness depends on the
     * type of the input value.
     * 
     * @return the indicator value
     */
    boolean isNot() {
        return not;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SqlProcessResult process(SqlProcessContext ctx) {

        SqlProcessResult result = new SqlProcessResult();
        boolean first = true;
        Object obj = ctx.dynamicInputValues;
        StringBuilder s = new StringBuilder(elements.size() * 32);
        s.append(IDENT_PREFIX);
        if (logger.isDebugEnabled())
            logger.debug("DataIdent process " + obj.getClass() + " " + obj + " " + this.sqlType);

        for (SqlMetaIdentItem item : this.elements) {
            if (first)
                first = false;
            else
                s.append(IDENT_SEPARATOR);
            s.append(item.getName());

            if (obj != null) {
                obj = SqlUtils.invokeMethod(obj, item.get(), item.is());
            }
            if (logger.isDebugEnabled())
                logger.debug("DataIdent process " + item + " " + ((obj != null) ? obj.getClass() : "null class") + " "
                        + obj + " " + item.get() + " " + item.is());
        }

        if (obj == null)
            result.addFalse();
        else if (obj instanceof Collection<?>)
            result.add(!((Collection<?>) obj).isEmpty());
        else if (obj.toString().length() <= 0)
            result.addFalse();
        else
            result.addTrue();

        result.addInputValue(s.substring(IDENT_PREFIX_LEN), new SqlInputValue(obj, this.caseConversion, this.sqlType));
        result.setSql(s);
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean processExpression(SqlProcessContext ctx) {
        Object obj = ctx.dynamicInputValues;

        if (logger.isDebugEnabled())
            logger.debug("DataIdent process expression " + obj.getClass() + " " + obj + " " + this.sqlType);

        for (SqlMetaIdentItem item : this.elements) {
            if (obj != null) {
                obj = SqlUtils.invokeMethod(obj, item.get(), item.is());
            }
            if (logger.isDebugEnabled())
                logger.debug("DataIdent process expression " + item + " "
                        + ((obj != null) ? obj.getClass() : "null class") + " " + obj + " " + item.get() + " "
                        + item.is());
        }

        boolean result = false;
        if (obj != null) {
            if (obj instanceof Boolean) {
                result = ((Boolean) obj).booleanValue();
            } else if (obj instanceof String) {
                String str = ((String) obj).trim();
                result = (str.length() > 0 && !str.toLowerCase().equals("false"));
            } else if (obj instanceof Number) {
                result = ((Number) obj).longValue() > 0;
            } else if (obj.getClass().isEnum() && this.sqlType == null) {
                result = true; // not null
            } else if (obj.getClass().isEnum() && this.sqlType != null) {
                if (obj.toString().equals(this.sqlType.getValue())) {
                    return true;
                } else if (sqlType.getMetaType() == SqlMetaType.ENUM_STRING) {
                    Method m = SqlUtils.getMethodEnumToValue(obj.getClass());
                    if (m != null)
                        return ((String) SqlUtils.invokeMethod(m, obj)).equals(this.sqlType.getValue());
                    else
                        return false;
                } else if (sqlType.getMetaType() == SqlMetaType.ENUM_INT) {
                    Method m = SqlUtils.getMethodEnumToValue(obj.getClass());
                    if (m != null)
                        return SqlUtils.invokeMethod(m, obj).toString().equals(this.sqlType.getValue());
                    else
                        return false;
                }
                result = true;
            } else {
                result = true; // not null
            }
        }
        return (this.not ? !result : result);
    }
}
